﻿// Picturez
//
// Nail.ImageConverter Library
// Nail framework
//
// Copyright © Frank Nagl, 2008-2012
// admin@franknagl.de
// www.franknagl.de
//
using System.Drawing.Drawing2D;

namespace Picturez
{
    using System;
    using System.Drawing;
    using System.Drawing.Imaging;
    using System.IO;

    /// <summary>
    /// Provides several static functions to convert one or multiple image file(s) in a 
    /// special image format.
    /// </summary>
    /// <remarks>
    /// <para>Supported formats:</para>
    /// <list type="enumeration">BMP</list>
    /// <list type="enumeration">EMF</list>
    /// <list type="enumeration">GIF</list>
    /// <list type="enumeration">JPG</list>
    /// <list type="enumeration">PNG</list>
    /// <list type="enumeration">TIFF</list>
    /// <list type="enumeration">WMF</list>
    /// </remarks>
    public static class ImageConverter
    {
        #region Public functions

        /// <summary>
        /// Converts 32 bpp bitmap to 24 bpp bitmap.
        /// </summary>
        /// <param name="source">The bitmap, which will be converted.</param>
        /// <returns>Result (24 bpp) bitmap.</returns>
        // ReSharper disable InconsistentNaming
        public static Bitmap ARGBToRGB(Bitmap source)
        // ReSharper restore InconsistentNaming
        {
            int w = source.Width;
            int h = source.Height;
            Rectangle rect = new Rectangle(0, 0, w, h);

            Bitmap destination = new Bitmap(w, h, PixelFormat.Format24bppRgb);
            BitmapData dstData = destination.LockBits
                (rect, ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);

            // lock source bitmap data
            BitmapData srcData = source.LockBits(
                rect, ImageLockMode.ReadOnly, source.PixelFormat);

            var dstOffset = dstData.Stride - w * 3;

            // process image
            unsafe
            {
                byte* src = (byte*)srcData.Scan0.ToPointer();
                byte* dst = (byte*)dstData.Scan0.ToPointer();
                // for each line
                for (int y = 0; y < h; y++)
                {
                    // for each pixel in line
                    for (int x = 0; x < w; x++, src += 4, dst += 3)
                    {
                        dst[RGBA.R] = src[RGBA.R];
                        dst[RGBA.G] = src[RGBA.G];
                        dst[RGBA.B] = src[RGBA.B];
                    }
                    dst += dstOffset;
                }
            }
            // unlock destination image
            source.UnlockBits(srcData);
            destination.UnlockBits(dstData);

            return destination;
        }

        /// <summary>
        /// Converts an image file to the passed encoding format.
        /// </summary>
        /// <param name="format">The encoding format.</param>
        /// <param name="imageFile">The image file.</param>
        /// <param name="overwrite">If <c>true</c> the
        /// original file will be overwritten or deleted.</param>
        /// <param name="jpegQuality">The JPEG quality (if JPEG encoder is choosen).
        /// Value has to be between 0 and 100.</param>
        /// <param name="highquality">Determines whether higher quality and 
        /// slower rendering, or lower quality and faster rendering will be used.
        /// </param>
        /// <returns>
        /// True if the converting process was successful, otherwise false.
        /// </returns>
        public static bool ConvertTo(ImageFormat format, string imageFile,
            bool overwrite, byte jpegQuality,
            bool highquality)
        {
            return Core(
                imageFile, 
                imageFile, 
                overwrite,
                0, 
                0, 
                format, 
                jpegQuality, 
                false, 
                highquality);
        }

        /// <summary>
        /// Converts an image file to the passed encoding format.
        /// </summary>
        /// <param name="format">The encoding format.</param>
        /// <param name="imageFile">The image file.</param>
        /// <param name="overwrite">If <c>true</c> the 
        /// original file will be overwritten or deleted.</param>
        /// <param name="jpegQuality">The JPEG quality (if JPEG encoder is choosen).
        /// Value has to be between 0 and 100.</param>
        /// <param name="biggerSideLength">Length of the bigger side of the image.</param>
        /// <param name="highquality">Determines whether higher quality and 
        /// slower rendering, or lower quality and faster rendering will be used.
        /// </param>
        /// <returns>
        /// True if the converting process was successful, otherwise false.
        /// </returns>
        public static bool ConvertTo(
            ImageFormat format, 
            string imageFile,
            bool overwrite, 
            byte jpegQuality,
            int biggerSideLength,
            bool highquality)
        {
            float w, h;
            CalcBiggerSideLength(imageFile, biggerSideLength, out w, out h);
            return Core(
                imageFile, 
                imageFile, 
                overwrite, 
                (int)w, 
                (int)h, 
                format, 
                jpegQuality, 
                false, 
                highquality);
        }

        /// <summary>
        /// Converts an image file to the passed encoding format.
        /// </summary>
        /// <param name="format">The encoding format.</param>
        /// <param name="imageFile">The image file.</param>
        /// <param name="overwrite">If <c>true</c> the
        /// original file will be overwritten or deleted.</param>
        /// <param name="jpegQuality">The JPEG quality (if JPEG encoder is choosen).
        /// Value has to be between 0 and 100.</param>
        /// <param name="width">The width.</param>
        /// <param name="height">The height.</param>
        /// <param name="correctForge">If <c>true</c> the aspect ratio will be hold, otherwise not.</param>
        /// <param name="highquality">Determines whether higher quality and 
        /// slower rendering, or lower quality and faster rendering will be used.
        /// </param>
        /// <returns>
        /// True if the converting process was successful, otherwise false.
        /// </returns>
        public static bool ConvertTo(
            ImageFormat format, 
            string imageFile, 
            bool overwrite,
            byte jpegQuality, 
            int width, 
            int height,
            bool correctForge,
            bool highquality)
        {
            return Core(
                imageFile, 
                imageFile, 
                overwrite, 
                width, 
                height, 
                format, 
                jpegQuality,
                correctForge, 
                highquality);
        }

        /// <summary>
        /// Converts an image file to the passed encoding format.
        /// </summary>
        /// <param name="format">The encoding format.</param>
        /// <param name="originalFile">The original file.</param>
        /// <param name="newFile">The new file.</param>
        /// <param name="jpegQuality">The JPEG quality (if JPEG encoder is choosen).
        /// Value has to be between 0 and 100.</param>
        /// <param name="deleteOriginal">True if the original image should be deleted
        /// after converting, otherwise false.</param>
        /// <param name="highquality">Determines whether higher quality and 
        /// slower rendering, or lower quality and faster rendering will be used.
        /// </param>
        /// <returns>
        /// True if the converting process was successful, otherwise false.
        /// </returns>
        public static bool ConvertTo(
            ImageFormat format, 
            string originalFile,
            string newFile, 
            byte jpegQuality,
            bool deleteOriginal,
            bool highquality)
        {
            return Core(
                originalFile, 
                newFile, 
                deleteOriginal, 
                0, 
                0, 
                format, 
                jpegQuality, 
                false, 
                highquality);
        }

        /// <summary>
        /// Converts an image file to the passed encoding format.
        /// </summary>
        /// <param name="format">The encoding format.</param>
        /// <param name="originalFile">The original file.</param>
        /// <param name="newFile">The new file.</param>
        /// <param name="jpegQuality">The JPEG quality (if JPEG encoder is choosen).
        /// Value has to be between 0 and 100.</param>
        /// <param name="biggerSideLength">Length of the bigger side of the image.</param>
        /// <param name="deleteOriginal">True if the original image should be deleted
        /// after converting, otherwise false.</param>
        /// <param name="highquality">Determines whether higher quality and 
        /// slower rendering, or lower quality and faster rendering will be used.
        /// </param>
        /// <returns>
        /// True if the converting process was successful, otherwise false.
        /// </returns>
        public static bool ConvertTo(
            ImageFormat format, 
            string originalFile,
            string newFile, 
            byte jpegQuality, 
            int biggerSideLength,
            bool deleteOriginal,
            bool highquality)
        {
            float w, h;
            CalcBiggerSideLength(originalFile, biggerSideLength, out w, out h);
            return Core(
                originalFile, 
                newFile, 
                deleteOriginal, 
                (int)w, 
                (int)h, 
                format, 
                jpegQuality, 
                false, 
                highquality);
        }

        /// <summary>
        /// Converts an image file to the passed encoding format.
        /// </summary>
        /// <param name="format">The encoding format.</param>
        /// <param name="originalFile">The original file.</param>
        /// <param name="newFile">The new file.</param>
        /// <param name="quality">The JPEG quality (if JPEG encoder is choosen).
        /// Value has to be between 0 and 100.</param>
        /// <param name="width">The width.</param>
        /// <param name="height">The height.</param>
        /// <param name="correctForge">If <c>true</c> the aspect ratio will be hold, otherwise not.</param>
        /// <param name="deleteOriginal">True if the original image should be deleted
        /// after converting, otherwise false.</param>
        /// <param name="highquality">Determines whether higher quality and 
        /// slower rendering, or lower quality and faster rendering will be used.
        /// </param>
        /// <returns>
        /// True if the converting process was successful, otherwise false.
        /// </returns>
        public static bool ConvertTo(
            ImageFormat format, 
            string originalFile, 
            string newFile,
            byte quality, 
            int width, 
            int height, 
            bool correctForge,
            bool deleteOriginal,
            bool highquality)
        {
            return Core(originalFile, newFile, deleteOriginal, width, height,
                format, quality, correctForge, highquality);
        }

        /// <summary>
        /// Sets the color palette of the <paramref name="bitmap"/> to gray.
        /// </summary>
        /// <param name="bitmap">The bitmap to set its color palette to gray.</param>
        public static void SetColorPaletteToGray(Bitmap bitmap)
        {
            // get palette
            ColorPalette cp = bitmap.Palette;
            // init palette
            for (int i = 0; i < 256; i++)
            {
                cp.Entries[i] = Color.FromArgb(i, i, i);
            }
            // set palette back
            bitmap.Palette = cp;
        }

        #endregion Public functions


        #region Private helper functions

        private static void CalcBiggerSideLength(string imageFile, 
                                                 int biggerSideLength,
                                                 out float w,
                                                 out float h)
        {
            Bitmap pic = new Bitmap(imageFile);
            w = pic.Width;
            h = pic.Height;
            float ratio = w / h;
            if (w > h)
            {
                w = biggerSideLength;
                h = w / ratio;
            }
            else
            {
                h = biggerSideLength;
                w = h * ratio;
            }
            pic.Dispose();
        }

        private static Bitmap ARGBTo8Bit(Bitmap source)
        {
            // check image format
            if (source.PixelFormat != PixelFormat.Format32bppRgb &&
                source.PixelFormat != PixelFormat.Format32bppArgb)
                throw new ArgumentException("Source image can be color (32 bpp) image only");

            int w = source.Width;
            int h = source.Height;
            Rectangle rect = new Rectangle(0, 0, w, h);

            Bitmap destination = new Bitmap(w, h, PixelFormat.Format8bppIndexed);
            BitmapData dstData = destination.LockBits
                (rect, ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed);

            // lock source bitmap data
            BitmapData srcData = source.LockBits(
                rect, ImageLockMode.ReadOnly, source.PixelFormat);
            var dstOffset = dstData.Stride - w;

            // process image
            unsafe
            {
                byte* src = (byte*)srcData.Scan0.ToPointer();
                byte* dst = (byte*)dstData.Scan0.ToPointer();
                // for each line
                for (int y = 0; y < h; y++)
                {
                    // for each pixel in line
                    for (int x = 0; x < w; x++, src += 4, dst++)
                    {
                        int avg = src[RGBA.R] + src[RGBA.G] + src[RGBA.B];
                        avg /= 3;
                        *dst = (byte)avg;
                    }
                    dst += dstOffset;
                }
            }
            // unlock destination image
            source.UnlockBits(srcData);
            destination.UnlockBits(dstData);
            SetColorPaletteToGray(destination);

            return destination;
        }

        private static Bitmap CloneBitmap(
            Bitmap source,
            int width,
            int height,
            PixelFormat format,
            bool highquality)
        {
            // create new image with desired pixel format
            Bitmap bitmap = new Bitmap(width, height, format);

            // draw source image on the new one using Graphics
            Graphics g = Graphics.FromImage(bitmap);

            if (highquality)
            {
                g.InterpolationMode = InterpolationMode.HighQualityBicubic;
                g.PixelOffsetMode = PixelOffsetMode.HighQuality;
                g.SmoothingMode = SmoothingMode.HighQuality;
            }
            else
            {
                g.InterpolationMode = InterpolationMode.Bilinear;
                g.PixelOffsetMode = PixelOffsetMode.HighSpeed;
                g.SmoothingMode = SmoothingMode.HighSpeed;
            }

            g.DrawImage(source, 0, 0, width, height);
            g.Dispose();

            return bitmap;
        }

        private static bool Core
            (String originalFile, 
             String newFile, 
             bool overwrite,
             int width, 
             int height, 
             ImageFormat format, 
             byte quality,
             bool correctForge,
            bool highquality)
        {
            try
            {
                Bitmap pic = new Bitmap(originalFile);                              
                //Store the original EXIF infos
                PropertyItem[] items = pic.PropertyItems;
                
                if (width == 0 || height == 0)
                {
                    width = pic.Width;
                    height = pic.Height;
                }
                float ratio = width / (float)height;
                float h = pic.Height;
                float w = pic.Width;
                float xStart = 0f;
                float yStart = 0f;
                float wCorrect = w;
                float hCorrect = h;

                if (correctForge)
                {
                    if (w / h > ratio)
                    {
                        wCorrect = h * ratio;
                        xStart = (w - wCorrect) / 2.0f;
                    }
                    else if (w / h <= ratio)
                    {
                        hCorrect = w / ratio;
                        yStart = (h - hCorrect) / 2.0f;
                    }
                }

                RectangleF rec = new RectangleF(xStart, yStart, wCorrect, hCorrect);
                Bitmap croppedPic = pic.Clone(rec, pic.PixelFormat);
                pic.Dispose();
                // do not use new Bitmap, it always makes 32 bit pixelformat
                // croppedPic = new Bitmap(croppedPic, width, height);
                if (IsColorImage(croppedPic.PixelFormat))
                {                 
                    croppedPic = CloneBitmap(croppedPic, width, height,
                                    croppedPic.PixelFormat, highquality);
                }
                else
                {
                    // 8 bit image
                    // OLD version
                    //croppedPic = new Bitmap(croppedPic, width, height);
                    //croppedPic = ARGBTo8Bit(croppedPic);

                    // since v2.1 (better quality also for grayscale)
                    croppedPic = new Bitmap(croppedPic);
                    croppedPic = CloneBitmap(croppedPic, width, height,
                                    croppedPic.PixelFormat, highquality);
                    croppedPic = ARGBTo8Bit(croppedPic);
                }
                Program.ReduceMemoryUsage();

                #region Correct the newFile name
                newFile = newFile.Substring(0, newFile.LastIndexOf('.') + 1) +
                    format.ToString().Replace("Jpeg", "jpg").ToLower();
                if (newFile.ToLower() != originalFile.ToLower())
                {
                    if (overwrite)
                        File.Delete(originalFile);
                }
                else if (!overwrite)
                {
                    newFile = newFile.Substring(0, newFile.LastIndexOf('.')) +
                    "_1." + format.ToString().Replace("Jpeg", "jpg").ToLower();
                }
                #endregion Correct the newFile name
                
                //Create destination directory, if it doesn't exist already
                newFile = newFile.Replace('\\', '/');
                int index = newFile.LastIndexOf('/');
                if (index != -1 && !Directory.Exists(newFile.Substring(0, index)))
                    Directory.CreateDirectory(newFile.Substring(0, index));

                //Before saving, set the original EXIF infos                
                foreach (PropertyItem item in items)                 
                    croppedPic.SetPropertyItem(item);

                //Saving processes
                if (format == ImageFormat.Jpeg)              
                    JpegEncoder.SaveJpeg(newFile, croppedPic, quality);
                else
                    croppedPic.Save(newFile, format);
                croppedPic.Dispose();                    
            }
            catch (Exception e)
            {
                MessageBoxOk.Show(e.Message, "", true);
                return false;
            }
            return true;
        }

        private static bool IsColorImage(PixelFormat format)
        {
            // check image format
            if (format != PixelFormat.Format24bppRgb &&
                format != PixelFormat.Format32bppArgb &&
                format != PixelFormat.Format32bppRgb &&
                format != PixelFormat.Format32bppPArgb)
            {
                return false;
            }
            return true;
        }
        #endregion Private helper functions
    }
}
